﻿using System;
using System.Diagnostics;
using System.IO;

namespace SfsTools.Core
{
    /// <summary>
    /// Implements a <see cref="IVolume"/> that is backed by a file.
    /// </summary>
    public class FileBackedVolume : Object, IVolume, IDisposable
    {
        #region Properties

        /// <summary>
        /// Contains the stream to access the file.
        /// </summary>
        private readonly Stream FStream;

        #endregion

        #region Methods

        /// <summary>
        /// Creates a new <see cref="FileBackedVolume"/>.
        /// </summary>
        /// <param name="path">The path to the image file.</param>
        /// <returns>A <see cref="FileBackedVolume"/> instance.</returns>
        [DebuggerStepThrough]
        public static FileBackedVolume Open(String path)
        {
            Stream stream = new System.IO.FileStream(path, FileMode.Open, FileAccess.ReadWrite, FileShare.Read);
            return new FileBackedVolume(stream);
        }

        /// <summary>
        /// Creates a new <see cref="FileBackedVolume"/>.
        /// </summary>
        /// <param name="path">The path to the image file.</param>
        /// <param name="length">The size of the image file in bytes.</param>
        /// <returns>A <see cref="FileBackedVolume"/> instance.</returns>
        [DebuggerStepThrough]
        public static FileBackedVolume OpenOrCreate(String path, long length)
        {
            Stream stream;
            try
            {
                stream = new System.IO.FileStream(path, FileMode.Open, FileAccess.ReadWrite, FileShare.Read);
                return new FileBackedVolume(stream);
            }
            catch (FileNotFoundException)
            {
                stream = new System.IO.FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read);
                return InitializeWithMarker(stream, 0xAA, length);
            }
        }

        /// <summary>
        /// Creates a new <see cref="FileBackedVolume"/>.
        /// </summary>
        /// <param name="path">The path to the image file.</param>
        /// <param name="length">The size of the image file in bytes.</param>
        /// <returns>A <see cref="FileBackedVolume"/> instance.</returns>
        [DebuggerStepThrough]
        public static FileBackedVolume Create(String path, long length)
        {
            Stream stream = new System.IO.FileStream(path, FileMode.Create, FileAccess.ReadWrite, FileShare.Read);
            return InitializeWithMarker(stream, 0xAA, length);
        }

        /// <summary>
        /// Creates a new <see cref="FileBackedVolume"/>.
        /// </summary>
        /// <param name="path">The path to the image file.</param>
        /// <param name="length">The size of the image file in bytes.</param>
        /// <param name="filler">The filler byte to initialize the image with.</param>
        /// <returns>A <see cref="FileBackedVolume"/> instance.</returns>
        [DebuggerStepThrough]
        public static FileBackedVolume Create(String path, long length, byte filler)
        {
            Stream stream = new System.IO.FileStream(path, FileMode.Create, FileAccess.ReadWrite, FileShare.Read);
            return InitializeWithMarker(stream, filler, length);
        }

        /// <summary>
        /// Creates a new <see cref="FileBackedVolume"/>.
        /// </summary>
        /// <param name="stream">The stream that represents the image file.</param>
        /// <param name="length">The size of the image file in bytes.</param>
        /// <returns>A <see cref="FileBackedVolume"/> instance.</returns>
        [DebuggerStepThrough]
        public static FileBackedVolume Create(Stream stream, long length)
        {
            stream.Position = 0;
            return InitializeWithMarker(stream, 0xAA, length);
        }

        /// <summary>
        /// Creates a new <see cref="FileBackedVolume"/>.
        /// </summary>
        /// <param name="stream">The stream that represents the image file.</param>
        /// <param name="length">The size of the image file in bytes.</param>
        /// <param name="filler">The filler byte to initialize the image with.</param>
        /// <returns>A <see cref="FileBackedVolume"/> instance.</returns>
        [DebuggerStepThrough]
        public static FileBackedVolume Create(Stream stream, long length, byte filler)
        {
            stream.Position = 0;
            return InitializeWithMarker(stream, filler, length);
        }

        /// <summary>
        /// Fills up the image file with marker bytes.
        /// </summary>
        /// <param name="stream">The stream.</param>
        /// <param name="filler">The filler byte.</param>
        /// <param name="length">How many bytes to fill.</param>
        /// <returns>A <see cref="FileBackedVolume"/> instance.</returns>
        [DebuggerStepThrough]
        private static FileBackedVolume InitializeWithMarker(Stream stream, byte filler, long length)
        {
            byte[] buffer = new byte[512];
            for (int i = 0; i < buffer.Length; i++)
                buffer[i] = filler;
            for (long i = 0; i < length / 512; i++)
                stream.Write(buffer, 0, buffer.Length);
            stream.Position = 0;
            return new FileBackedVolume(stream);
        }

        #endregion

        #region Constructors
        /// <overloads>Initializes a new instance of <see cref="FileBackedVolume"/>.</overloads>
        /// <summary>
        /// Initializes a new instance of <see cref="FileBackedVolume"/>.
        /// </summary>
        /// <param name="stream">The stream to access the file.</param>
        /// <exception cref="ArgumentNullException">The argument <paramref name="stream"/> is <see langword="null"/>.</exception>
        [DebuggerStepThrough]
        public FileBackedVolume(Stream stream)
            : base()
        {
            if (stream == null)
                throw new ArgumentNullException("stream");
            FStream = stream;
        }
        /// <summary>
        /// Initializes a new instance of <see cref="FileBackedVolume"/>.
        /// </summary>
        /// <param name="path">The path to the file.</param>
        /// <exception cref="ArgumentNullException">The argument <paramref name="path"/> is <see langword="null"/>.</exception>
        [DebuggerStepThrough]
        public FileBackedVolume(String path)
            : base()
        {
            FStream = new System.IO.FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read);
        }
        /// <summary>
        /// Initializes a new instance of <see cref="FileBackedVolume"/>.
        /// </summary>
        /// <param name="path">The path to the file.</param>
        /// <param name="size">The length of the volume, in bytes.</param>
        /// <exception cref="ArgumentNullException">The argument <paramref name="path"/> is <see langword="null"/>.</exception>
        [DebuggerStepThrough]
        public FileBackedVolume(String path, long size)
            : base()
        {
            FStream = new System.IO.FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read);
            FStream.SetLength(size);
        }
        #endregion

        #region IVolume Members

        /// <summary>
        /// Gets the length of the volume, in bytes.
        /// </summary>
        /// <exception cref="NotSupportedException">An attempt was made to query the size of the volume on a volume which does not support this operation.</exception>
        Int64 IVolume.Length
        {
            [DebuggerStepThrough]
            get
            {
                return FStream.Length;
            }
        }

        /// <summary>
        /// Reads <paramref name="count"/> bytes from the volume.
        /// </summary>
        /// <param name="offset">The byte offset to start reading from.</param>
        /// <param name="count">The count of bytes read.</param>
        /// <returns>An array contaning the bytes read.</returns>
        /// <exception cref="EndOfStreamException">There is not enough space to read all the bytes (more bytes requested than the volume contains).</exception>
        /// <exception cref="ArgumentOutOfRangeException">The argument <paramref name="count"/> is out of valid range.</exception>
        /// <exception cref="IOException">An I/O error occurred.</exception>
        [DebuggerStepThrough]
        byte[] IVolume.Read(long offset, int count)
        {
            if ((count < 0))
                throw new ArgumentOutOfRangeException("count");
            if (count > FStream.Length - offset)
            {
                throw new EndOfStreamException();
            }
            else
            {
                byte[] buffer = new byte[count];
                FStream.Position = offset;
                if (FStream.Read(buffer, 0, count) < count)
                    throw new EndOfStreamException();
                return buffer;
            }
        }

        /// <summary>
        /// Writes the contents of <paramref name="buffer"/> to the volume.
        /// </summary>
        /// <param name="offset">The byte offset to start writing at.</param>
        /// <param name="buffer">The array containing the bytes to write.</param>
        /// <exception cref="EndOfStreamException">There is not enough space to write all the bytes.</exception>
        /// <exception cref="ArgumentNullException">The argument <paramref name="buffer"/> is <see langword="null"/>.</exception>
        /// <exception cref="IOException">An I/O error occurred.</exception>
        [DebuggerStepThrough]
        void IVolume.Write(long offset, byte[] buffer)
        {
            if (buffer == null)
                throw new ArgumentNullException("buffer");
            if (buffer.Length > FStream.Length - offset)
            {
                throw new EndOfStreamException();
            }
            else
            {
                FStream.Position = offset;
                FStream.Write(buffer, 0, buffer.Length);
            }
        }

        /// <summary>
        /// Writes the contents of <paramref name="buffer"/> to the volume.
        /// </summary>
        /// <param name="offset">The byte offset to start writing at.</param>
        /// <param name="buffer">The array containing the bytes to write.</param>
        /// <param name="index">The index into <paramref name="buffer"/> to copy the bytes from.</param>
        /// <param name="count">The count of bytes to copy.</param>
        /// <exception cref="EndOfStreamException">There is not enough space to write all the bytes.</exception>
        /// <exception cref="ArgumentNullException">The argument <paramref name="buffer"/> is <see langword="null"/>.</exception>
        /// <exception cref="IOException">An I/O error occurred.</exception>
        [DebuggerStepThrough]
        void IVolume.Write(long offset, byte[] buffer, int index, int count)
        {
            if (buffer == null)
                throw new ArgumentNullException("buffer");
            if (buffer.Length > FStream.Length - offset)
            {
                throw new EndOfStreamException();
            }
            else
            {
                FStream.Position = offset;
                FStream.Write(buffer, 0, buffer.Length);
            }
        }

        #endregion

        #region IDisposable Members

        /// <summary>
        /// Releases all resources used by this <see cref="FileBackedVolume"/> instance.
        /// </summary>
        [DebuggerStepThrough]
        public void Dispose()
        {
            FStream.Dispose();
        }

        /// <summary>
        /// Releases all resources used by this <see cref="FileBackedVolume"/> instance.
        /// </summary>
        [DebuggerStepThrough]
        void IDisposable.Dispose()
        {
            FStream.Dispose();
        }

        #endregion
    }
}
